package dproxy.server.net.protocol;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.Map.Entry;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import dproxy.server.common.exceptions.AppInfrastructureException;
import dproxy.server.common.exceptions.ResponseReadException;
import dproxy.server.net.backoff.Timeout;

/**
 * Reader for HTTP Protocol. Not a true HTTP 1.1 implementation because
 * of DProxy's architecture. At every request the connection is closed.
 */
// TODO [4] test using an HTTP proxy/retest locally
// TODO [5] Run CheckStyle before new release
public class HttpProtocol extends BaseProtocol {

    /** Logger. */
    private Log logger = LogFactory.getLog(getClass());


    private static class HttpMessage {
    	/** Either a Request-Line (for requests) or a Status-Line (for responses) */
    	String firstLine;
    	Map<String, String> headers;
    	byte [] messageBody;
    }

    /**
     * @see dproxy.server.net.protocol.Protocol#getName()
     */
    public String getName() {
        return "HTTP";
    }
    
    /**
     * @see dproxy.server.net.protocol.BaseProtocol#init()
     */
    @Override
    public void init() {
        super.init();
        if (logger.isDebugEnabled()) {
            logger.debug("Initiating HTTP Protocol");
        }
    }

    /**
     * @see dproxy.server.net.protocol.Protocol#generateKey(byte[])
     */
    public String generateKey(byte[] request) {
        try {
			ByteArrayInputStream bais = new ByteArrayInputStream(request);
			BufferedReader reader = new BufferedReader(new InputStreamReader(bais));
			String firstLine = reader.readLine();
			StringBuffer keyBuffer = new StringBuffer();
	        keyBuffer.append(getName());
	        keyBuffer.append("#");
	        keyBuffer.append(firstLine);
	        String key = keyBuffer.toString();
			return key.substring(0, getKeyMaxLen(key.getBytes()));
		} catch (IOException e) {
			throw new AppInfrastructureException(e);
		}
    }

    /**
     * @see dproxy.server.net.protocol.Protocol#readRequest(InputStream)
     */
    public byte[] readRequest(InputStream inputStream) {
        byte[] bs;
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
        try {
	        HttpMessage request = readHttpMessage(reader);
	        baos.write(request.firstLine.getBytes());
			baos.write("\n".getBytes());
			for (Entry<String, String> header: request.headers.entrySet()) {
				String headerLine = header.getKey() + ": " + header.getValue();
				baos.write(headerLine.getBytes());
				baos.write("\n".getBytes());
			}
			// Separator CRLF line:
			baos.write("\n".getBytes());
			if (request.messageBody != null) {
				baos.write(request.messageBody);
			}
			
        } catch (IOException e) {
            throw new AppInfrastructureException(e);
        }
        
        bs = baos.toByteArray();
        return bs;
    }

    /**
     * A message is formed of:
     * (Request-line | Status-line )
     * * (( general-header | request-header | response-header | entity-header ) CRLF)
     * CRLF
     * [ message-body ]
     */
	private HttpMessage readHttpMessage(BufferedReader reader) throws IOException {
		HttpMessage message = new HttpMessage();
		String requestLine = reader.readLine();
		message.firstLine = requestLine;
		
		message.headers = new HashMap<String, String>();
		// Read headers:
		while (true) {
			String headerLine = reader.readLine();
			// Stop when finding CRLF
			if (headerLine == null || headerLine.trim().equals("")) {
				break;
			} else {
				String headerName = headerLine.substring(0, headerLine.indexOf(':'));
				String headerValue = headerLine.substring(headerLine.indexOf(':') + 2); // +2 to cut ": "
				message.headers.put(headerName, headerValue); // TODO put as it's on RFC
			}
		}
		
		// TODO this is ugly
		int contentLengh = -1;
		if (message.headers.containsKey("Content-Length")) {
			contentLengh = Integer.parseInt(message.headers.get("Content-Length"));
		} else if (message.headers.containsKey("Content-length")) {
			contentLengh = Integer.parseInt(message.headers.get("Content-length"));
		}
		
		if (contentLengh > 0) {
			// Read message body:
			message.messageBody = new byte[contentLengh];
			for (int i = 0; i < contentLengh; i++) {
				message.messageBody[i] = (byte) reader.read();
			}
		}
		return message;
	}

	/**
     * @see dproxy.server.net.protocol.Protocol#readResponse(InputStream)
     */
    public byte[] readResponse(InputStream inputStream)
            throws ResponseReadException {
        byte[] bs;
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
        
        try {
        	Timeout.getInstance().waitForDataAvailable(inputStream);
	        HttpMessage response = readHttpMessage(reader);
	        baos.write(response.firstLine.getBytes());
			baos.write("\n".getBytes());
			for (Entry<String, String> header: response.headers.entrySet()) {
				String headerLine = header.getKey() + ": " + header.getValue();
				baos.write(headerLine.getBytes());
				baos.write("\n".getBytes());
			}
			// Separator CRLF line:
			baos.write("\n".getBytes());
			if (response.messageBody != null) {
				baos.write(response.messageBody);
			}
			
			checkHttpResponse(response, baos);
        } catch (IOException e) {
            throw new AppInfrastructureException(e);
        }
        
        bs = baos.toByteArray();
        return bs;
    }

    /**
     * Check if it's a valid cacheable HTTP response 
     */
	private void checkHttpResponse(HttpMessage response, ByteArrayOutputStream baos) throws ResponseReadException {
		StringTokenizer tokenizer = new StringTokenizer(response.firstLine, " ");
		String version = tokenizer.nextToken();
		String statusCode = tokenizer.nextToken();
		StringBuffer scBuffer = new StringBuffer();
		while (tokenizer.hasMoreTokens()) {
			scBuffer.append(tokenizer.nextToken());
			if (tokenizer.hasMoreTokens()) {
				scBuffer.append(" ");
			}
		}
		String reason = scBuffer.toString();
		if (logger.isDebugEnabled()) {
			logger.debug("Response : " +
					"Version : [" + version +
					"] Status code : [" + statusCode +
					"] Reason phrase : [" + reason + "]");
		}
		
		// Refactoring Dproxy to allow a correct HTTP "no-cache" headers implementation:
		//
		// "Cache-Control" (private, no-cache and no-store), "pragma" (no-cache)
		// and "expires" HTTP headers are ignored because of DProxy's architecture.
		// By the RFC, this headers can be present both on requests and on responses,
		// but DPRoxy readRequest() method is not able to ask to "dont store". Implementing 
		// this will require change on DProxy's architecture (like readRequest being able 
		// to signal a "no store" through flag or exception)
		//
		
		// Difference from RFC: Only 2xx states are cached.
		if (!statusCode.startsWith("2")) {
			throw new ResponseReadException(
					baos.toByteArray(), 
					"Not cacheable code (" + statusCode + ")");
		}
	}

    /**
     * The first line of a HTTP request is formed of:
     * Method SP Request-URI SP HTTP-Version CRLF
     * @see dproxy.server.net.protocol.BaseProtocol#parseRequestAddress(byte[])
     */
	@Override
	public URI parseRequestAddress(byte[] request) {
		URI remoteURI;
		try {
			ByteArrayInputStream bais = new ByteArrayInputStream(request);
			BufferedReader reader = new BufferedReader(new InputStreamReader(bais));
			HttpMessage httpRequest = readHttpMessage(reader);
			
			StringTokenizer tokenizer = new StringTokenizer(httpRequest.firstLine, " ");
			String verb = tokenizer.nextToken();
			String resource = tokenizer.nextToken();
			String protocolVersion = tokenizer.nextToken();
			if (logger.isDebugEnabled()) {
				logger.debug("Request: " +
						"Verb : [" + verb + "] " +
						"Host : [" + resource + "] " +
						"Version : [" + protocolVersion + "] ");
			}
			String host = httpRequest.headers.get("Host");
			
			if (host == null) {
				remoteURI = new URI(resource);
			} else {
				URI remoteHost = new URI(host);
				URI remoteResource = new URI(resource);
				remoteURI = remoteHost.relativize(remoteResource);
			}
		} catch (IOException e) {
			throw new AppInfrastructureException(e);
		} catch (URISyntaxException e) {
			throw new AppInfrastructureException(e);
		}
		return remoteURI;
	}
    
}
